package cn.demoncat.util.dingtalk;

import cn.demoncat.util.dingtalk.codec.DingTalkCallbackCrypto;
import cn.demoncat.util.dingtalk.config.DingTalkProperties;
import cn.demoncat.util.dingtalk.constant.DingTalkRedisConstant;
import cn.demoncat.util.dingtalk.event.DingTalkProcessInstanceChangeEvent;
import cn.demoncat.util.exception.BizRuntimeException;
import cn.demoncat.util.lang.JsonUtil;
import cn.demoncat.util.lang.constant.LongConstant;
import cn.demoncat.util.redis.util.RedisUtil;
import cn.demoncat.util.web.SpringUtil;
import com.alibaba.fastjson.JSONObject;
import com.aliyun.dingtalkoauth2_1_0.models.GetAccessTokenRequest;
import com.aliyun.dingtalkworkflow_1_0.models.*;
import com.aliyun.teautil.models.RuntimeOptions;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;
import java.util.List;
import java.util.Map;
import java.util.Objects;

/**
 * 钉钉工具
 *
 * 开放平台（应用管理）：https://open-dev.dingtalk.com
 * 管理后台（数据管理）：https://oa.dingtalk.com
 * 接口文档：https://open.dingtalk.com/document/orgapp/api-overview
 * 错误诊断（request id）：https://open-dev.dingtalk.com/fe/api-tools#/troubleshoot
 *
 * @author 延晓磊
 * @since 2023年03月23日
 */
@Component
public class DingTalkUtil {

    @Resource
    private DingTalkCallbackCrypto dingTalkCallbackCrypto;
    @Resource
    private com.aliyun.dingtalkoauth2_1_0.Client dingTalkAuthClient;
    @Resource
    private com.aliyun.dingtalkworkflow_1_0.Client dingTalkWorkflowClient;
    @Resource
    private DingTalkProperties dingTalkProperties;
    @Resource
    private RedisUtil redisUtil;
    @Resource
    private SpringUtil springUtil;

    /**
     * 获取AccessToken
     *
     * 1、accessToken的有效期为7200秒，有效期内重复获取会返回相同结果并自动续期，过期后获取会返回新的accessToken。
     * 2、开发者需要缓存accessToken，用于后续接口的调用；因为每个应用的accessToken是独立的，所以进行缓存时需要区分应用来进行存储。
     *
     * @return accessToken
     *
     * @author 延晓磊
     * @since 2023年03月23日
     */
    public String getAccessToken(){
        return redisUtil.getStr(DingTalkRedisConstant.ACCESS_TOKEN, () ->{
            GetAccessTokenRequest request = new GetAccessTokenRequest().setAppKey(dingTalkProperties.getAppKey()).setAppSecret(dingTalkProperties.getAppSecret());
            try {
                return dingTalkAuthClient.getAccessToken(request).getBody().getAccessToken();
            } catch (Exception e) {
                throw new BizRuntimeException("请求钉钉失败（获取AccessToken）：" + e.getMessage(), e);
            }
        });
    }

    /**
     * 订阅事件
     *
     * 1、保存事件订阅配置时，由钉钉发起校验请求（POST）
     * 2、触发订阅事件时，由钉钉发起通知请求
     * 3、需要返回带"success"的加密文本
     *
     * 1、需要在应用管理中订阅事件，并实例化 cn.demoncat.util.dingtalk.event 中的 Event
     *
     * 订阅流程：开发者后台 > 进入应用 > 事件与回调 > 配置密钥、接口 > 勾选事件
     * 事件文档：https://open.dingtalk.com/document/orgapp/configure-event-subcription
     *
     * @param signature 消息体签名
     * @param timestamp 时间戳
     * @param nonce     随机字符串
     * @param body      消息体：{encrypt:加密的事件信息}
     *
     * @return 成功响应
     *
     * @author 延晓磊
     * @since 2023年03月23日
     */
    public String callback(String signature, String timestamp, String nonce, Map<String,String> body){
        // 解密参数
        JSONObject param = JsonUtil.parseObject(dingTalkCallbackCrypto.getDecryptMsg(signature, timestamp, nonce, body.get("encrypt")));
        // 分发事件
        switch (Objects.requireNonNull(param).getString("EventType")) {
            case DingTalkProcessInstanceChangeEvent.EventType:
                // 审批流程结束
                int processInstanceChangeStatus = "terminate".equals(param.getString("type")) ? 3 : ("finish".equals(param.getString("type")) ? ("agree".equals(param.getString("result")) ? 1 : 2) : 0);
                if (processInstanceChangeStatus > 0) {
                    springUtil.getBean(DingTalkProcessInstanceChangeEvent.class, event -> event.consumer(DingTalkProcessInstanceChangeEvent.Data.build(param.getString("processInstanceId"), processInstanceChangeStatus)));
                }
                break;
            default:
                break;
        }
        // 返回 success
        return JsonUtil.toString(dingTalkCallbackCrypto.getEncryptedMap("success"));
    }

    /**
     * 工作流-创建流程表单（模板）
     *
     * 1、每个企业最多创建200个审批模板，超过最大数量后调用接口会报错。
     * 2、如果更新的组件在流程设计中设置为分支条件，则该模板不支持修改。
     * 3、创建表单后，需要通过【钉钉管理后台/OA审批/模板管理】进行流程配置。
     *
     * @param name  表单名称
     * @param components 表单控件
     *
     * @return processCode 表单模板Code，企业内唯一
     *
     * @author 延晓磊
     * @since 2023年03月23日
     */
    public String createProcessForm(String name, List<FormComponent> components){
        return this.updateProcessForm(null, name, components);
    }

    /**
     * 工作流-修改流程表单（模板）
     *
     * 常见错误：模板名称重复、应用密钥错误、未配置IP白名单、未开通审批权限
     *
     * @param processCode   表单模板Code：为空时新增，非空时修改
     * @param name          表单名称
     * @param components    表单控件
     *
     * @return processCode 表单模板Code，企业内唯一
     *
     * @author 延晓磊
     * @since 2023年03月23日
     */
    public String updateProcessForm(String processCode, String name, List<FormComponent> components){
        // 请求头
        FormCreateHeaders headers = new FormCreateHeaders();
        headers.xAcsDingtalkAccessToken = this.getAccessToken();
        // 表单配置
        FormCreateRequest.FormCreateRequestTemplateConfig config = new FormCreateRequest.FormCreateRequestTemplateConfig();
        config.disableFormEdit = dingTalkProperties.getDisableFormEdit();
        config.disableResubmit = dingTalkProperties.getDisableResubmit();
        config.disableDeleteProcess = dingTalkProperties.getDisableDeleteProcess();
        config.disableStopProcessButton = dingTalkProperties.getDisableStopProcessButton();
        // 请求参数
        FormCreateRequest request = new FormCreateRequest().setProcessCode(processCode).setName(name).setFormComponents(components).setTemplateConfig(config);
        try {
            return dingTalkWorkflowClient.formCreateWithOptions(request, headers, new RuntimeOptions()).getBody().getResult().getProcessCode();
        } catch (Exception e) {
            throw new BizRuntimeException("请求钉钉失败（"+ (StringUtils.isBlank(processCode)?"创建":"修改") +"审批模板）：" + e.getMessage(), e);
        }
    }

    /**
     * 工作流-发起流程实例
     *
     * 1、发起审批实例后，审批人可以通过OA审批-待处理的和待办-我的待办进行查看
     * 2、复用审批后台设置的审批流
     *
     * 常见错误：userId错误
     *
     * @param processCode   表单模板Code
     * @param userId        审批发起人的userId
     * @param values        审批参数
     *
     * @return instanceId   流程实例ID
     *
     * @author 延晓磊
     * @since 2023年03月23日
     */
    public String startProcessInstance(String processCode, String userId, List<StartProcessInstanceRequest.StartProcessInstanceRequestFormComponentValues> values){
        // 请求头
        StartProcessInstanceHeaders headers = new StartProcessInstanceHeaders();
        headers.xAcsDingtalkAccessToken = this.getAccessToken();
        // 请求参数
        StartProcessInstanceRequest request = new StartProcessInstanceRequest().setOriginatorUserId(userId).setProcessCode(processCode).setDeptId(LongConstant.N_1).setMicroappAgentId(dingTalkProperties.getAgentId()).setFormComponentValues(values);
        try {
            return dingTalkWorkflowClient.startProcessInstanceWithOptions(request, headers, new RuntimeOptions()).getBody().getInstanceId();
        } catch (Exception e) {
            throw new BizRuntimeException("请求钉钉失败（发起审批）：" + e.getMessage(), e);
        }
    }

    /**
     * 工作流-查看流程实例
     *
     * @param instanceId    流程实例ID
     *
     * @return 流程实例详情
     *
     * @author 延晓磊
     * @since 2023年03月23日
     */
    public GetProcessInstanceResponseBody.GetProcessInstanceResponseBodyResult getProcessInstance(String instanceId){
        // 请求头
        GetProcessInstanceHeaders headers = new GetProcessInstanceHeaders();
        headers.xAcsDingtalkAccessToken = this.getAccessToken();
        // 请求参数
        GetProcessInstanceRequest request = new GetProcessInstanceRequest().setProcessInstanceId(instanceId);
        try {
            return dingTalkWorkflowClient.getProcessInstanceWithOptions(request, headers, new RuntimeOptions()).getBody().getResult();
        } catch (Exception e) {
            throw new BizRuntimeException("请求钉钉失败（查看审批）：" + e.getMessage(), e);
        }
    }

    /**
     * 工作流-撤销流程实例
     *
     * 1、审批实例的状态为流程中。
     * 2、审批发起15秒内不能撤销审批流程。
     *
     * @param instanceId    流程实例ID
     * @param remarks       终止说明
     *
     * @return 是否撤销成功
     *
     * @author 延晓磊
     * @since 2023年03月23日
     */
    public Boolean cancelProcessInstance(String instanceId, String remarks){
        // 请求头
        TerminateProcessInstanceHeaders headers = new TerminateProcessInstanceHeaders();
        headers.xAcsDingtalkAccessToken = this.getAccessToken();
        // 请求参数
        TerminateProcessInstanceRequest request = new TerminateProcessInstanceRequest().setProcessInstanceId(instanceId).setIsSystem(true).setRemark(remarks);
        try {
            return dingTalkWorkflowClient.terminateProcessInstanceWithOptions(request, headers, new RuntimeOptions()).getBody().getResult();
        } catch (Exception e) {
            throw new BizRuntimeException("请求钉钉失败（撤销审批）：" + e.getMessage(), e);
        }
    }

    /**
     * 构建审批流程表单控件 - 文本输入框
     *
     * 1、如果需要隐藏控件，可以设置 component.getProps().setInvisible(true)
     *
     * @param name          标题
     * @param placeholder   占位文字
     * @param required      是否必填
     * @return
     *
     * @author 延晓磊
     * @since 2023年03月24日
     */
    public static FormComponent buildProcessFormTextComponent(String name, String placeholder, boolean required){
        return new FormComponent().setComponentType("TextField").setProps(new FormComponentProps().setLabel(name).setPlaceholder(placeholder).setRequired(required));
    }

    /**
     * 构建审批流程表单控件值 - 文本输入框
     *
     * @param name  标题
     * @param value 值
     * @return
     *
     * @author 延晓磊
     * @since 2023年03月27日
     */
    public static StartProcessInstanceRequest.StartProcessInstanceRequestFormComponentValues buildProcessFormTextComponentValue(String name, String value){
         return new StartProcessInstanceRequest.StartProcessInstanceRequestFormComponentValues().setComponentType("TextField").setName(name).setValue(value);
    }
}
